// 模拟线上弹窗嵌入环境
// 反向代理 & 请求拦截 & 内容替换

import http from "http";
import https from "https";
import zlib from "zlib";
import fs from "fs";

/**
 * @typedef {http.IncomingMessage} Request
 * @typedef {http.ServerResponse} Response
 * @typedef {(req: Request, res: Response) => void | Promise<void>} Middleware
 */

class Server {
  /** @type {*} */
  instance;
  /** @type {{ method: "*" | "GET" | "POST", path: string, handler: Middleware }[]} */
  routes = [];
  /**
   * @type {{ port: number }}
   */
  options;

  /**
   * @param {{ port: number }} options
   */
  constructor(options) {
    this.options = options || {};
  }

  /**
   * @param {string | Middleware} arg1
   * @param {Middleware?} arg2
   */
  use(arg1, arg2) {
    let path, handler;
    if (typeof arg1 === "string") {
      path = arg1;
      handler = arg2;
    } else {
      path = "*";
      handler = arg1;
    }
    this.routes.push({ method: "*", path, handler });
    return this;
  }
  /**
   * @param {string} path
   * @param {Middleware} handler
   */
  get(path, handler) {
    this.routes.push({ method: "GET", path, handler });
    return this;
  }
  /**
   * @param {string} path
   * @param {Middleware} handler
   */
  post(path, handler) {
    this.routes.push({ method: "POST", path, handler });
    return this;
  }
  /**
   * @param {string} path
   * @param {Middleware} handler
   */
  put(path, handler) {
    this.routes.push({ method: "PUT", path, handler });
    return this;
  }

  start() {
    console.log(`http://localhost:${this.options.port}`);
    this.instance = http
      .createServer(async (req, res) => {
        const { method: _method, url } = req;
        const len = this.routes.length;
        for (let i = 0; i < len; i++) {
          const { method, path, handler } = this.routes[i];
          if (
            (path === "*" ||
              url === path ||
              (path !== "/" && url.startsWith(path))) &&
            (method === "*" || method === _method)
          ) {
            await handler(req, res);
          }
          if (res.headersSent || res.body) {
            console.log(_method + (method === "*" ? "*" : ""), url);
            break;
          }
        }
        if (res.body) {
          if (res.body && typeof res.body === "object") {
            res.setHeader("content-type", "application/json");
            res.body = JSON.stringify(res.body);
          }
          res.end(res.body);
        } else if (!res.headersSent) {
          res.writeHead(404, {
            "content-type": "text/plain",
          });
          res.end("endpoint notfound");
        }
      })
      .on("error", (error) => {
        console.log("!!ERROR!!");
        console.log(error);
      })
      .listen(this.options.port);
  }
}

class ProxyTargets {
  /** @type {Record<string, string>} */
  proxys = {};

  /** @type {string[]} */
  whitelists = [];
  /** @type {string} */
  host;

  /**
   * @param {{
   * whitelists: string[];
   * host: string;
   * }} options
   */
  constructor(options) {
    const { whitelists = [], host } = options || {};
    this.whitelists = whitelists;
    this.host = host;
  }

  /**
   * @param {string} target
   * @param {string} url
   */
  direct(target, url) {
    this.proxys[target] = url;
    console.log(`add proxy: ${target} ==> ${url}`);
    return target;
  }

  /**
   * @param {string} target
   * @param {string} url
   */
  append(url) {
    const _target = Object.keys(this.proxys).find(
      (_t) => this.proxys[_t] === url,
    );
    if (_target) {
      return _target;
    }
    const random = () =>
      (Math.random() * Math.PI).toString(16).replace(".", "");
    return this.direct("/" + random(), url);
  }

  /** @param {string} target  */
  find(target) {
    const _target = Object.keys(this.proxys).find((_t) =>
      target.startsWith(_t),
    );
    if (!_target) {
      return null;
    }
    return target.replace(_target, this.proxys[_target]);
  }
  /**
   * @param {string | Buffer | undefined | null} input
   */
  handlePossibleProxys(input) {
    if (typeof input !== "string") {
      return input;
    }
    const reg = /https?:\/\/(?!localhost)[^"/]+(?=(\/|"))/g;
    return input.replace(reg, (match) => {
      if (this.whitelists.includes(match)) {
        return match;
      }
      return `${this.host}${this.append(match)}`;
    });
  }

  /**
   * @param {string} url
   * @param {http.IncomingMessage} req
   * @returns {Promise<{
   *   headers: http.IncomingHttpHeaders;
   *   stream?: http.IncomingMessage;
   *   body?: string;
   * }>}
   */
  async proxy(url, req) {
    const { method, headers } = req;
    return new Promise((resolve, reject) => {
      const _url = new URL(url);
      const h = _url.protocol === "http:" ? http : https;
      const _req = h.request(
        new URL(url),
        {
          method,
          headers: { ...headers, origin: _url.origin, host: _url.host },
        },
        (res) => {
          if (res.statusCode !== 200) {
            console.log(
              "[PROXY INFO]:",
              "Request failed with status code",
              res.statusCode,
            );
            return reject();
          }
          const contentType = res.headers["content-type"];
          const contentEncoding = res.headers["content-encoding"];
          const transferEncoding = res.headers["transfer-encoding"];
          const isJson = /json/.test(contentType);
          const isText =
            !/stream/.test(contentType) && (isJson || /text/.test(contentType));
          let readable = res;
          if (/gzip/.test(contentEncoding)) {
            delete res.headers["content-encoding"];
            readable = res.pipe(zlib.createGunzip());
          }
          if (res.headers["content-length"]) {
            delete res.headers["content-length"];
          }
          const resolved = {
            headers: {
              ...res.headers,
              "access-control-allow-origin": "*",
              "access-control-allow-method": "*",
            },
          };
          if (!isText || transferEncoding === "chunked") {
            return resolve({ ...resolved, stream: readable });
          }
          let data = [];
          readable.on("error", reject);
          readable.on("data", (chunk) => {
            data = [...data, ...chunk];
          });
          readable.on("end", () => {
            resolve({
              ...resolved,
              body: Buffer.from(data).toString("utf8"),
            });
          });
        },
      );
      _req.on("error", reject);
      req.pipe(_req);
    }).catch(() => {
      return {};
    });
  }
}

const server = new Server({ port: 8888 });
const proxys = new ProxyTargets({
  host: `http://localhost:8888`,
  whitelists: ["http://www.w3.org"],
});

const target = "http://112.80.18.150:28005";

server
  .get("/", async (req, res) => {
    const { headers, body } = await proxys.proxy(target, req);
    res.writeHead(200, {
      ...headers,
    });
    res.end(proxys.handlePossibleProxys(body));
  })
  .use("*", async (req, res) => {
    const target = proxys.find(req.url);
    if (!target) {
      return;
    }
    // catch this file
    if (target.endsWith("/assets/index-rtHtZiM8.js")) {
      const files = fs.readdirSync("./dist/assets");
      const index = files.find((file) => /^index.+\.js$/.test(file));
      if (index) {
        res.writeHead(200, {
          "content-type": "application/javascript",
        });
        fs.createReadStream(`./dist/assets/${index}`).pipe(res);
      }
      return;
    }
    const { headers, body, stream } = await proxys.proxy(target, req);
    if (!headers) {
      return;
    }
    res.writeHead(200, {
      ...headers,
    });
    if (body) {
      res.body = proxys.handlePossibleProxys(body);
    } else {
      stream.pipe(res);
    }
  })
  .start();
